package com.nulldev.util.web.YouTubeExtractor.video;

import static com.nulldev.util.manage.Macros.__assert;

import java.util.Arrays;
import java.util.Map;

import com.nulldev.util.VariableAPI.MathUtil;
import com.nulldev.util.VariableAPI.RandomUtil;
import com.nulldev.util.VariableAPI.StringsUtil;
import com.nulldev.util.data.Variables;
import com.nulldev.util.internal.backport.concurrency9.Lists;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.manage.MarkedForRemoval;
import com.nulldev.util.web.URLUtils.URLUtils;
import com.nulldev.util.web.YouTubeExtractor.formats.Format;
import com.nulldev.util.web.YouTubeExtractor.formats.ThreeSixtyMode;
import com.nulldev.util.web.YouTubeExtractor.formats.ecd.codecs.AudioCodecs;
import com.nulldev.util.web.YouTubeExtractor.formats.ecd.codecs.VideoCodecs;

public class YouTubeVideo {
	private static final Logger log = LoggerFactory.getLogger();

	public static enum FormatType {
		/**
		 * Standard single file stream.
		 */
		STANDARD(false),
		/**
		 * YouTube's own stream format (??) <br>
		 * <b>WARNING</b>: Contains no init and index payloads meaning that segmented
		 * downloading is a maybe.
		 */
		OTF_STREAM(true),
		/**
		 * HLS stream.
		 */
		M3U8_STREAM(true),
		/**
		 * MPEG-DASH stream.
		 */
		DASH_STREAM(true),
		UNKNOWN(false)

		;

		private final boolean isStream;

		private FormatType(final boolean isStream) {
			this.isStream = isStream;
		}

		public boolean isStream() {
			return this.isStream;
		}
	}

	private final String url;

	private boolean requiresSupplimentaryVideoAudio;
	private boolean isLiveStream;
	private boolean requiresInternalProxy;
	private String codecMime = "unknown/unknown";

	private int itag;
	private int fps = 25; // Rounded up from 24
	private int bitrate = 80000;
	private long fileSize;
	private double targetDuration = 5;

	private String initPayload = null, indexPayload = null, size = "0x0";
	private FormatType type = FormatType.STANDARD;

	private final Format f;

	public YouTubeVideo(final String url, final boolean requiresSuppVidAudio, final int itag, final Format f) {
		this.url = StringsUtil.cleanInvalidCharacters(url);
		this.requiresSupplimentaryVideoAudio = requiresSuppVidAudio;
		this.itag = itag;
		this.f = f;
	}

	/**
	 * @returns URL link for the specific video type. (String)
	 */
	public String videoURL() {
		return __process_url(false);
	}

	/**
	 * @returns URL link for the specific video type. (String)
	 * @param enableBackendRandomization - Enables a 50% chance to use an
	 *                                   alternative CDN.
	 */
	public String videoURL(final boolean enableBackendRandomization) {
		return __process_url(enableBackendRandomization);
	}

	private String __process_url(final boolean enableBackendRandomization) {
		final StringBuilder output = new StringBuilder(this.url);
		if (enableBackendRandomization && this.hasFallbackURLs() && (RandomUtil.randInt(0, 100) >= 51)) {
			/* Do a random CDN. */
			final String alt = this.fallbackVideoURL(false);
			if (alt != null) {
				output.setLength(0);
				output.append(alt);
			} else {
				log.warn("__process_url: Failed to generate fallback video URL...");
			}
		}
		return output.toString();
	}

	/**
	 * @returns: true - This is most likely an audio or video only stream, you'll
	 *           need another one for true playback. false - This is a fallback
	 *           stream and contains both audio and video, but is limited to lower
	 *           resolutions.
	 */
	public boolean requiresSupplimentaryVideoAudio() {
		return this.requiresSupplimentaryVideoAudio;
	}

	/**
	 * @returns The ID Tag for the video type (int)
	 */
	public int getTag() {
		return this.itag;
	}

	/**
	 * @returns Format (itag) information for the stream.
	 */
	public Format getFormat() {
		return this.f;
	}

	/**
	 * [!] Soon to be replaced by getFormatType() <br>
	 * 
	 * @returns true - If this video is a livestream only entry (HLS or DASH) <br>
	 *          false - A standard YouTube video stream.
	 */
	@Deprecated
	@MarkedForRemoval(reason = "Replaced by getFormatType()")
	public boolean isLiveStream() {
		return this.isLiveStream;
	}

	@Deprecated
	@MarkedForRemoval(reason = "Replaced by setFormatType()")
	public YouTubeVideo setLivestream(final boolean isLiveStream) {
		this.isLiveStream = isLiveStream;
		return this;
	}

	/**
	 * [!] Used only by livestreams.
	 * 
	 * @returns true - If built in HLS/DASH conversion is needed (nTube only) <br>
	 *          false - No conversion is needed.
	 */
	public boolean requiresInternalProxy() {
		return this.requiresInternalProxy;
	}

	public YouTubeVideo setInternalProxy(final boolean internalProxy) {
		this.requiresInternalProxy = internalProxy;
		return this;
	}

	public YouTubeVideo setCodecMime(final String mime) {
		this.codecMime = mime;
		return this;
	}

	/**
	 * @returns The mime for the stream. (String)
	 */
	public String getCodecMime() {
		return this.codecMime;
	}

	public boolean hasCodecMime() {
		return this.codecMime != null && !this.codecMime.equalsIgnoreCase("unknown/unknown");
	}

	/**
	 * @returns The mime for the stream with any quotations removed. (String)
	 */
	public String getPrettyMime() {
		if (this.codecMime.equalsIgnoreCase("unknown/unknown"))
			return "unknown";
		if (this.codecMime.contains("\""))
			return StringsUtil.substringBetweenO(this.codecMime, "\"", "\"");
		return this.codecMime;
	}

	/**
	 * @returns The framerate (FPS) of the video.
	 */
	public int getFPS() {
		return this.fps;
	}

	public YouTubeVideo setFPS(final int fps) {
		this.fps = fps;
		return this;
	}

	@Override
	public String toString() {
		return "YouTubeVideo[format=" + this.f + ",type=" + this.type + ",rsva=" + this.requiresSupplimentaryVideoAudio + ",itag=" + this.itag + ",url="
				+ this.url + ",evc=" + this.e_vcodec + ",eac=" + this.e_acodec + ",hash=" + MathUtil.toHex(super.hashCode()) + "]";
	}

	public YouTubeVideo setBitrate(final int bitrate) {
		this.bitrate = bitrate;
		return this;
	}

	public int getBitrate() {
		return this.bitrate;
	}

	public YouTubeVideo setIndexPayload(final String index) {
		this.indexPayload = index;
		return this;
	}

	public String getIndexPayload() {
		return this.indexPayload;
	}

	public YouTubeVideo setInitPayload(final String init) {
		this.initPayload = init;
		return this;
	}

	public String getInitPayload() {
		return this.initPayload;
	}

	public YouTubeVideo setSize(final String size) {
		this.size = size;
		return this;
	}

	public String getSize() {
		return this.size;
	}

	/**
	 * @returns The total length of the stream (same as the clen URL parameter)
	 *          (long)
	 */
	public long getFileSize() {
		return this.fileSize;
	}

	public YouTubeVideo setFileSize(final long size) {
		this.fileSize = size;
		return this;
	}

	public YouTubeVideo setTargetDuration(final double dur) {
		this.targetDuration = dur;
		return this;
	}

	public double getTargetDuration() {
		return this.targetDuration;
	}

	public boolean validSegmentPayloads() {
		if (this.initPayload == null || this.indexPayload == null)
			return false;
		if (this.initPayload.isEmpty() || this.indexPayload.isEmpty())
			return false;
		if (this.initPayload.equalsIgnoreCase("null") || this.indexPayload.equalsIgnoreCase("null"))
			return false;
		return true;
	}

	public FormatType getFormatType() {
		return this.type;
	}

	public YouTubeVideo setFormatType(final FormatType t) {
		if (t == null)
			return this;
		this.type = t;
		return this;
	}

	public boolean isLiveFormat() {
		return this.type == FormatType.DASH_STREAM || this.type == FormatType.M3U8_STREAM || this.type == FormatType.OTF_STREAM;
	}

	private VideoCodecs e_vcodec = VideoCodecs.UNKNOWN;
	private AudioCodecs e_acodec = AudioCodecs.UNKNOWN;

	public YouTubeVideo setECDCodecsFromMime(final String mime) {
		if (mime == null || mime.isEmpty())
			return this;
		this.e_vcodec = VideoCodecs.fromMime(mime);
		this.e_acodec = AudioCodecs.fromMime(mime);
		return this;
	}

	public VideoCodecs getEnhancedVideoCodec() {
		return this.e_vcodec;
	}

	public AudioCodecs getEnhancedAudioCodec() {
		return this.e_acodec;
	}

	/* Fallback Videos API */
	/*
	 * Shamelessly stolen (and adapted) from
	 * https://github.com/yt-dlp/yt-dlp/pull/5639/
	 */

	public boolean hasFallbackURLs() {
		return this.url != null && this.url.contains("&fvip=") && this.url.contains("&mn=");
	}

	public String fallbackVideoURL() {
		return fallbackVideoURL(true);
	}

	public String fallbackVideoURL(final boolean doDFSFix) {
		final Map<String, String> query = URLUtils.parseQuery(Variables.requireNonNullObject(this.url, "url"));
		final String replacementVip = query.get("fvip");
		__assert(replacementVip == null, "replacementVip == null");
		final String mn = query.get("mn");
		__assert(mn == null, "mn == null");
		final String[] altIds = mn.split(",");
		// Apparently the order is random between [ORIGINAL, FALLBACK]
		if (altIds == null || altIds.length != 2)
			return null;
		final String currentFVip = StringsUtil.substringBetweenO(this.url, "rr", "--");
		__assert(currentFVip == null, "currentFVip == null");
		/* YouTube started to serve two character F_VIP's in May 2024 */
		__assert(currentFVip.length() > 2, "currentFVip::length [" + currentFVip + "] > 2");
		__assert(currentFVip.length() < 1, "currentFVip::length [" + currentFVip + "] < 1");
		final String currentId = StringsUtil.substringBetweenO(this.url, "---", ".googlevideo");
		__assert(currentId == null, "currentId == null");
		final String replacementId = Lists.of(altIds).stream().filter(id -> !id.equalsIgnoreCase(currentId)).findFirst().get();
		final String newUrl = this.url.replace("rr" + currentFVip + "--", "rr" + replacementVip + "--")
				.replace("---" + currentId + ".googlevideo", "---" + replacementId + ".googlevideo").concat("&fallback_count=1");
		log.debug("fallbackVideoURL: [currentFVip=" + currentFVip + ",currentId=" + currentId + ",replacementVip=" + replacementVip + "] altIds {"
				+ Arrays.toString(altIds) + "}");
		return newUrl;
	}

	/* ABI(6080) */

	private ThreeSixtyMode projectionMode = ThreeSixtyMode.NONE;

	public ThreeSixtyMode getProjectionMode() {
		return this.projectionMode;
	}
}
